Skip to content

fix: prevent file handle leak when maxFiles is exceeded#1068

Open
guoyangzhen wants to merge 1 commit intonode-formidable:masterfrom
guoyangzhen:master
Open

fix: prevent file handle leak when maxFiles is exceeded#1068
guoyangzhen wants to merge 1 commit intonode-formidable:masterfrom
guoyangzhen:master

Conversation

@guoyangzhen
Copy link
Copy Markdown
Contributor

Fixes #987

Problem

When options.maxFiles is set and a request exceeds the limit, file handles are leaked and never released until the process exits.

Root cause in _handlePart:

  1. this.emit('fileBegin', part.name, file) — triggers the maxFiles check
  2. maxFiles check calls this._error(), destroying files in this.openedFiles
  3. But _handlePart continues and calls file.open() + this.openedFiles.push(file)
  4. This new file stream is opened after the error handling ran, so it's never closed

Fix

Check this.error after emitting fileBegin but before file.open(). If an error occurred (e.g., maxFiles exceeded), decrement _flushing and return early to prevent opening the file stream.

Reproduction

import formidable from 'formidable'

const form = formidable({
  maxFiles: 1,
  uploadDir: './uploads',
  keepExtensions: true,
})

// Upload 2 files — the 2nd file's write stream is never closed
form.parse(req)

With the fix, no file handles are leaked when maxFiles is exceeded.

Fixes node-formidable#987

When maxFiles limit is reached, the fileBegin event handler calls _error(),
but _handlePart continues and opens a write stream for the new file.
These file handles are never closed.

Fix: check this.error after emitting fileBegin and before file.open().
If an error occurred (e.g., maxFiles exceeded), decrement _flushing
and return early to prevent the file stream from being opened.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a resource-leak edge case in IncomingForm._handlePart() where exceeding options.maxFiles could trigger _error() during fileBegin, but _handlePart() would still proceed to file.open(), creating an untracked file handle.

Changes:

  • Add an early-return guard after emitting fileBegin to stop processing if an error was raised (e.g., maxFiles exceeded).
  • Decrement _flushing on this early-return path to keep the internal “in-flight” counter balanced.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Formidable.js

// Check for error after fileBegin (e.g., maxFiles exceeded) to avoid leaking file handles
if (this.error) {
this._flushing -= 1;
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the multipart path, but the same maxFiles/fileBegin sequencing exists in the octet-stream plugin (src/plugins/octetstream.js): it emits fileBegin before file.open() and openedFiles.push(file). If maxFiles is exceeded there, _error() will run before the file is tracked and the handle can still leak. Consider applying the same this.error short-circuit (and matching _flushing handling) in the octet-stream code path as well to make the fix comprehensive.

Suggested change
this._flushing -= 1;

Copilot uses AI. Check for mistakes.
Comment thread src/Formidable.js
Comment on lines +406 to +410
// Check for error after fileBegin (e.g., maxFiles exceeded) to avoid leaking file handles
if (this.error) {
this._flushing -= 1;
return;
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing Jest tests asserting maxFiles emits an error, but they don’t cover the new behavior that prevents file.open()/handle creation after fileBegin triggers _error(). Please add a test that reproduces the leak scenario (e.g., stub _newFile() to return a file with an open spy and ensure open is not called for the part that exceeds maxFiles, and/or assert no streams remain open).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File handle not released after options.maxFiles check

2 participants